implement basic telegram agent

Tom König преди 8 години
родител
ревизия
adc1dee4fb
променени са 4 файла, в които са добавени 232 реда и са изтрити 11 реда
  1. 12 11
      Gemfile
  2. 26 0
      Gemfile.lock
  3. 92 0
      app/models/agents/telegram_agent.rb
  4. 102 0
      spec/models/agents/telegram_agent_spec.rb

+ 12 - 11
Gemfile

@@ -25,17 +25,18 @@ end
25 25
 
26 26
 # Optional libraries.  To conserve RAM, comment out any that you don't need,
27 27
 # then run `bundle` and commit the updated Gemfile and Gemfile.lock.
28
-gem 'twilio-ruby', '~> 3.11.5'    # TwilioAgent
29
-gem 'ruby-growl', '~> 4.1.0'      # GrowlAgent
30
-gem 'net-ftp-list', '~> 3.2.8'    # FtpsiteAgent
31
-gem 'wunderground', '~> 1.2.0'    # WeatherAgent
32
-gem 'forecast_io', '~> 2.0.0'     # WeatherAgent
33
-gem 'rturk', '~> 2.12.1'          # HumanTaskAgent
34
-gem 'hipchat', '~> 1.2.0'         # HipchatAgent
35
-gem 'xmpp4r',  '~> 0.5.6'         # JabberAgent
36
-gem 'mqtt'                        # MQTTAgent
37
-gem 'slack-notifier', '~> 1.0.0'  # SlackAgent
38
-gem 'hypdf', '~> 1.0.7'           # PDFInfoAgent
28
+gem 'twilio-ruby', '~> 3.11.5'       # TwilioAgent
29
+gem 'ruby-growl', '~> 4.1.0'         # GrowlAgent
30
+gem 'net-ftp-list', '~> 3.2.8'       # FtpsiteAgent
31
+gem 'wunderground', '~> 1.2.0'       # WeatherAgent
32
+gem 'forecast_io', '~> 2.0.0'        # WeatherAgent
33
+gem 'rturk', '~> 2.12.1'             # HumanTaskAgent
34
+gem 'hipchat', '~> 1.2.0'            # HipchatAgent
35
+gem 'xmpp4r',  '~> 0.5.6'            # JabberAgent
36
+gem 'mqtt'                           # MQTTAgent
37
+gem 'slack-notifier', '~> 1.0.0'     # SlackAgent
38
+gem 'hypdf', '~> 1.0.7'              # PDFInfoAgent
39
+gem 'telegram-bot-ruby', '~> 0.4.1'  # TelegramAgent
39 40
 
40 41
 # Weibo Agents
41 42
 gem 'weibo_2', github: 'cantino/weibo_2', branch: 'master'

+ 26 - 0
Gemfile.lock

@@ -112,6 +112,10 @@ GEM
112 112
       multi_json (>= 1.0.0)
113 113
     aws-sdk-core (2.2.15)
114 114
       jmespath (~> 1.0)
115
+    axiom-types (0.1.1)
116
+      descendants_tracker (~> 0.0.4)
117
+      ice_nine (~> 0.11.0)
118
+      thread_safe (~> 0.3, >= 0.3.1)
115 119
     bcrypt (3.1.10)
116 120
     better_errors (1.1.0)
117 121
       coderay (>= 1.0.0)
@@ -146,6 +150,8 @@ GEM
146 150
     chronic (0.10.2)
147 151
     cliver (0.3.2)
148 152
     coderay (1.1.0)
153
+    coercible (1.0.0)
154
+      descendants_tracker (~> 0.0.1)
149 155
     coffee-rails (4.1.1)
150 156
       coffee-script (>= 2.2.0)
151 157
       railties (>= 4.0.0, < 5.1.x)
@@ -170,6 +176,8 @@ GEM
170 176
       activesupport (>= 3.0, < 5.0)
171 177
     delorean (2.1.0)
172 178
       chronic
179
+    descendants_tracker (0.0.4)
180
+      thread_safe (~> 0.3, >= 0.3.1)
173 181
     devise (3.5.4)
174 182
       bcrypt (~> 3.0)
175 183
       orm_adapter (~> 0.1)
@@ -225,6 +233,8 @@ GEM
225 233
       dotenv (>= 0.7)
226 234
       thor (>= 0.13.6)
227 235
     formatador (0.2.5)
236
+    gene_pool (1.4.1)
237
+      thread_safe
228 238
     geokit (1.8.5)
229 239
       multi_json (>= 1.3.2)
230 240
     geokit-rails (2.0.1)
@@ -281,6 +291,7 @@ GEM
281 291
     hypdf (1.0.7)
282 292
       httmultiparty (= 0.3.10)
283 293
     i18n (0.7.0)
294
+    ice_nine (0.11.2)
284 295
     jmespath (1.1.3)
285 296
     jquery-rails (3.1.3)
286 297
       railties (>= 3.0, < 5.0)
@@ -371,6 +382,11 @@ GEM
371 382
       multi_json (~> 1.3)
372 383
       omniauth-oauth (~> 1.0)
373 384
     orm_adapter (0.5.0)
385
+    persistent_http (1.0.6)
386
+      gene_pool (>= 1.3)
387
+    persistent_httparty (0.1.2)
388
+      httparty (~> 0.9)
389
+      persistent_http (< 2)
374 390
     pg (0.18.3)
375 391
     poltergeist (1.8.1)
376 392
       capybara (~> 2.1)
@@ -515,6 +531,10 @@ GEM
515 531
       net-ssh (>= 2.8.0)
516 532
     string-scrub (0.0.5)
517 533
     systemu (2.6.4)
534
+    telegram-bot-ruby (0.4.1)
535
+      httmultiparty
536
+      persistent_httparty
537
+      virtus
518 538
     term-ansicolor (1.3.0)
519 539
       tins (~> 1.0)
520 540
     therubyracer (0.12.2)
@@ -559,6 +579,11 @@ GEM
559 579
       macaddr (~> 1.0)
560 580
     uuidtools (2.1.5)
561 581
     vcr (2.9.2)
582
+    virtus (1.0.5)
583
+      axiom-types (~> 0.1)
584
+      coercible (~> 1.0)
585
+      descendants_tracker (~> 0.0, >= 0.0.3)
586
+      equalizer (~> 0.0, >= 0.0.9)
562 587
     warden (1.2.4)
563 588
       rack (>= 1.0)
564 589
     webmock (1.17.4)
@@ -664,6 +689,7 @@ DEPENDENCIES
664 689
   spring (~> 1.6.3)
665 690
   spring-commands-rspec (~> 1.0.4)
666 691
   string-scrub
692
+  telegram-bot-ruby (~> 0.4.1)
667 693
   therubyracer (~> 0.12.2)
668 694
   tumblr_client!
669 695
   twilio-ruby (~> 3.11.5)

+ 92 - 0
app/models/agents/telegram_agent.rb

@@ -0,0 +1,92 @@
1
+require 'telegram/bot'
2
+require 'open-uri'
3
+require 'tempfile'
4
+
5
+module Agents
6
+  class TelegramAgent < Agent
7
+    cannot_be_scheduled!
8
+    cannot_create_events!
9
+    no_bulk_receive!
10
+
11
+    gem_dependency_check { defined?(Telegram) }
12
+
13
+    description <<-MD
14
+      #{'# Include `telegram-bot-ruby` in your Gemfile to use this Agent!' if dependencies_missing?}
15
+
16
+      The Telegram Agent receives and collects events and sends them via [Telegram](https://telegram.org/).
17
+
18
+      It is assumed that events have either a `text`, `photo`, `audio`, `document` or `video` key. You can use the EventFormattingAgent if your event does not provide these keys.
19
+
20
+      The value of `text` key is sent as a plain text message.
21
+      The value of `photo`, `audio`, `document` and `video` keys should be an url which contents are sent to you according to the type.
22
+
23
+      **Setup**
24
+
25
+      1. obtain an `auth_token` by [creating a new bot](https://telegram.me/botfather).
26
+      2. [send a private message to your bot](https://telegram.me/YourHuginnBot)
27
+      3. obtain your private `chat_id` [from the recently started conversation](https://api.telegram.org/bot<auth_token>/getUpdates)
28
+    MD
29
+
30
+    def default_options
31
+      {
32
+        auth_token: 'xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
33
+        chat_id: 'xxxxxxxx'
34
+      }
35
+    end
36
+
37
+    def validate_options
38
+      errors.add(:base, 'auth_token is required') unless options['auth_token'].present?
39
+      errors.add(:base, 'chat_id is required') unless options['chat_id'].present?
40
+    end
41
+
42
+    def working?
43
+      received_event_without_error? && !recent_error_logs?
44
+    end
45
+
46
+    def receive(incoming_events)
47
+      incoming_events.each do |event|
48
+        receive_event event
49
+      end
50
+    end
51
+
52
+    private
53
+
54
+    TELEGRAM_FIELDS = {
55
+      text:     :send_message,
56
+      photo:    :send_photo,
57
+      audio:    :send_audio,
58
+      document: :send_document,
59
+      video:    :send_video
60
+    }.freeze
61
+
62
+    def receive_event(event)
63
+      TELEGRAM_FIELDS.each do |field, method|
64
+        payload = load_field event, field
65
+        next unless payload
66
+        send_telegram_message method, field => payload
67
+      end
68
+    end
69
+
70
+    def send_telegram_message(method, params)
71
+      params[:chat_id] = interpolated['chat_id']
72
+      Telegram::Bot::Client.run interpolated['auth_token'] do |bot|
73
+        bot.api.send method, params
74
+      end
75
+    end
76
+
77
+    def load_field(event, field)
78
+      payload = event.payload[field]
79
+      return false unless payload.present?
80
+      return payload if field == :text
81
+      load_file payload
82
+    end
83
+
84
+    def load_file(url)
85
+      file = Tempfile.new [File.basename(url), File.extname(url)]
86
+      file.binmode
87
+      file.write open(url).read
88
+      file.rewind
89
+      file
90
+    end
91
+  end
92
+end

+ 102 - 0
spec/models/agents/telegram_agent_spec.rb

@@ -0,0 +1,102 @@
1
+require 'rails_helper'
2
+
3
+describe Agents::TelegramAgent do
4
+  before do
5
+    default_options = {
6
+      auth_token: 'xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
7
+      chat_id: 'xxxxxxxx'
8
+    }
9
+    @checker = Agents::TelegramAgent.new name: 'Telegram Tester', options: default_options
10
+    @checker.user = users(:bob)
11
+    @checker.save!
12
+
13
+    @sent_messages = []
14
+    stub_methods
15
+  end
16
+
17
+  def stub_methods
18
+    stub.any_instance_of(Agents::TelegramAgent).send_telegram_message do |method, params|
19
+      @sent_messages << { method => params }
20
+    end
21
+
22
+    stub.any_instance_of(Agents::TelegramAgent).load_file do |_url|
23
+      :stubbed_file
24
+    end
25
+  end
26
+
27
+  def event_with_payload(payload)
28
+    event = Event.new
29
+    event.agent = agents(:bob_weather_agent)
30
+    event.payload = payload
31
+    event.save!
32
+    event
33
+  end
34
+
35
+  describe 'validation' do
36
+    before do
37
+      expect(@checker).to be_valid
38
+    end
39
+
40
+    it 'should validate presence of of auth_token' do
41
+      @checker.options[:auth_token] = ''
42
+      expect(@checker).not_to be_valid
43
+    end
44
+
45
+    it 'should validate presence of of chat_id' do
46
+      @checker.options[:chat_id] = ''
47
+      expect(@checker).not_to be_valid
48
+    end
49
+  end
50
+
51
+  describe '#receive' do
52
+    it 'processes multiple events properly' do
53
+      event_0 = event_with_payload text: 'Looks like its going to rain'
54
+      event_1 = event_with_payload text: 'Another text message'
55
+      @checker.receive [event_0, event_1]
56
+
57
+      expect(@sent_messages).to eq([
58
+        { send_message: { text: 'Looks like its going to rain' } },
59
+        { send_message: { text: 'Another text message' } }
60
+      ])
61
+    end
62
+
63
+    it 'accepts photo key and uses :send_photo to send the file' do
64
+      event = event_with_payload photo: 'https://example.com/image.png'
65
+      @checker.receive [event]
66
+
67
+      expect(@sent_messages).to eq([{ send_photo: { photo: :stubbed_file } }])
68
+    end
69
+
70
+    it 'accepts audio key and uses :send_audio to send the file' do
71
+      event = event_with_payload audio: 'https://example.com/sound.mp3'
72
+      @checker.receive [event]
73
+
74
+      expect(@sent_messages).to eq([{ send_audio: { audio: :stubbed_file } }])
75
+    end
76
+
77
+    it 'accepts document key and uses :send_document to send the file' do
78
+      event = event_with_payload document: 'https://example.com/document.pdf'
79
+      @checker.receive [event]
80
+
81
+      expect(@sent_messages).to eq([{ send_document: { document: :stubbed_file } }])
82
+    end
83
+
84
+    it 'accepts video key and uses :send_video to send the file' do
85
+      event = event_with_payload video: 'https://example.com/video.avi'
86
+      @checker.receive [event]
87
+
88
+      expect(@sent_messages).to eq([{ send_video: { video: :stubbed_file } }])
89
+    end
90
+  end
91
+
92
+  describe '#working?' do
93
+    it 'is not working without having received an event' do
94
+      expect(@checker).not_to be_working
95
+    end
96
+
97
+    it 'is working after receiving an event without error' do
98
+      @checker.last_receive_at = Time.now
99
+      expect(@checker).to be_working
100
+    end
101
+  end
102
+end